/*
 *  tracker/ScopesControl.cpp
 *
 *  Copyright 2009 Peter Barth
 *
 *  This file is part of Milkytracker.
 *
 *  Milkytracker is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Milkytracker is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Milkytracker.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 *  ScopesControl.cpp
 *  MilkyTracker
 *
 *  Created by Peter Barth on 28.10.05.
 *
 */

#include "ScopesControl.h"
#include "Screen.h"
#include "GraphicsAbstract.h"
#include "Font.h"
#include "Button.h"
#include "PlayerController.h"
#include "Tools.h"
#include "PPUIConfig.h"
#include "TrackerConfig.h"

#undef PANNINGINDICATOR

pp_int32 ScopesControl::WRAPCHANNELS()
{
    if (parentScreen->getWidth() < 800)
    {
        #ifdef __LOWRES__
        return 16;
        #else
        return 18;
        #endif
    }
    if (parentScreen->getWidth() < 1024)
    {
        return 20;
    }
    if (parentScreen->getWidth() < 1280)
    {
        return 22;
    }

    return 32;
}

ScopesControl::ScopesControl (pp_int32 id,
                              PPScreen* parentScreen,
                              EventListenerInterface* eventListener,
                              const PPPoint& location, const PPSize& size,
                              bool border /*= true*/) :
    PPControl(id, parentScreen, eventListener, location, size),
    border(border),
    borderColor(&ourOwnBorderColor),
    playerController(NULL),
    numChannels(0),
    enabled(true),
    appearance(AppearanceTypeNormal),
    currentClickType(ClickTypeMute)
{
    // default color
    color.set(0, 0, 0);

    ourOwnBorderColor.set(192, 192, 192);

    visibleWidth = size.width - 2;
    visibleHeight = size.height - 4;

    font = PPFont::getFont(PPFont::FONT_SYSTEM);
    smallFont = PPFont::getFont(PPFont::FONT_TINY);

    lastNumChannels = 0;
    memset(onOffState, 0, sizeof(onOffState));

    memset(muteChannels, 0, sizeof(muteChannels));
    memset(lastMuteChannels, 0, sizeof(lastMuteChannels));

    memset(recChannels, 0, sizeof(recChannels));
    memset(lastRecChannels, 0, sizeof(lastRecChannels));

    lMouseDownInChannel = rMouseDownInChannel = -1;

    backgroundButton = new PPButton(0, parentScreen, NULL, PPPoint(location.x, location.y), PPSize(size.width, size.height), false, false);
    backgroundButton->setColor(PPUIConfig::getInstance()->getColor(PPUIConfig::ColorListBoxBackground));
    backgroundButton->setInvertShading(true);
}

ScopesControl::~ScopesControl ()
{
    delete backgroundButton;
}

static inline pp_int32 myMod(pp_int32 a, pp_int32 b)
{
    pp_int32 res = a % b;
    if (res < 0) res += b;
    return res;
}

class ScopePainter : public PlayerController::SampleDataFetcher
{
    private:
        PPGraphicsAbstract* g;
        const pp_uint32 count;
        const pp_uint32 channelHeight;
        const PPColor& scopebColor;
        const PPColor& scopedColor;
        pp_int32 locx, locy;
        const ScopesControl::AppearanceTypes appearance;
        bool flipped;
        pp_int32 counter, flipCounter, flipCounterStep;
        pp_int32 count2;
        pp_int32 sr, sg, sb;
        pp_int32 addr, addg, addb;
        pp_int32 lasty, lastx;
    public:
        ScopePainter (PPGraphicsAbstract* g, pp_uint32 count, pp_uint32 channelHeight,
                      const PPColor& scopebColor, const PPColor& scopedColor,
                      pp_int32 locx, pp_int32 locy,
                      ScopesControl::AppearanceTypes appearance) :
            g(g),
            count(count),
            channelHeight(channelHeight),
            scopebColor(scopebColor),
            scopedColor(scopedColor),
            locx(locx), locy(locy),
            appearance(appearance),
            flipped(false),
            counter(0), flipCounter(0), flipCounterStep(1)
        {
            count2 = count - 3;
            if (count2 < 2)
                count2 = 2;
            flipCounter = 0;
            sr = scopedColor.r * 65536;
            sg = scopedColor.g * 65536;
            sb = scopedColor.b * 65536;
            addr = (scopebColor.r - scopedColor.r) * 65536 / (count2 / 2);
            addg = (scopebColor.g - scopedColor.g) * 65536 / (count2 / 2);
            addb = (scopebColor.b - scopedColor.b) * 65536 / (count2 / 2);

            lasty = locy;
            lastx = -1;
        }

        virtual void fetchSampleData(mp_sint32 sample)
        {
            const pp_int32 y = (((-sample >> 10) * (signed)channelHeight) >> 6) + locy;

            g->setSafeColor(sr >> 16, sg >> 16, sb >> 16);
            sr += addr; sg += addg; sb += addb;
            if (flipCounter >= (count2 >> 1) - 1 && !flipped)
            {
                flipped = true;
                addr = -addr;
                addg = -addg;
                addb = -addb;
            }
            flipCounter += flipCounterStep;

            switch (appearance)
            {
                case ScopesControl::AppearanceTypeNormal:
                    g->setPixel(locx, y);
                    g->setColor(0, 0, 0);
                    g->setPixel(locx, y + 1);
                    break;

                case ScopesControl::AppearanceTypeSolid:
                    if (y < locy)
                    {
                        g->drawVLine(y, locy, locx);
                        g->setColor(0, 0, 0);
                        g->setPixel(locx, locy);
                    }
                    else if (y > locy)
                    {
                        g->drawVLine(locy, y, locx);
                        g->setColor(0, 0, 0);
                        g->setPixel(locx, y);
                    }
                    else
                    {
                        g->setPixel(locx, y);
                        g->setColor(0, 0, 0);
                        g->setPixel(locx, y + 1);
                    }
                    break;

                case ScopesControl::AppearanceTypeLines:
                    if (!(counter & 1) || (counter == (signed)count - 1))
                    {
                        if (lastx == -1)
                        {
                            g->setPixel(locx, y);
                            g->setColor(0, 0, 0);
                            g->setPixel(locx, y + 1);
                        }
                        else
                        {
                            g->drawAntialiasedLine(lastx, lasty, locx, y);
                        }
                        lasty = y;
                        lastx = locx;
                    }
                    counter++;

            }

            locx++;
        }

        pp_int32 getLocx() const {
            return locx;
        }
};

void ScopesControl::paint(PPGraphicsAbstract* g)
{
    if (!isVisible())
        return;

    PPColor bColor = *borderColor, dColor = *borderColor;
    // adjust dark color
    dColor.scaleFixed(32768);

    // adjust bright color
    bColor.scaleFixed(87163);

    pp_int32 xOffset = 2;
    pp_int32 yOffset = 2;

    backgroundButton->paint(g);

    g->setRect(location.x, location.y, location.x + size.width + 1, location.y + size.height + 1);

    if (border)
    {
        drawThickBorder(g, *borderColor);
    }

    g->setRect(location.x + 2, location.y + 2, location.x + size.width - 2, location.y + size.height - 2);

    //g->setColor(0,0,0);
    //g->fill();

    if (!playerController || numChannels < 2)
        return;

    PPColor foregroundColor = PPUIConfig::getInstance()->getColor(PPUIConfig::ColorStaticText);

    pp_int32 channelWidth = visibleWidth / numChannels;
    pp_int32 channelHeight = visibleHeight;

    if (numChannels > WRAPCHANNELS())
    {
        channelWidth = visibleWidth / (numChannels >> 1);
        channelHeight >>= 1;

        pp_int32 i;
        pp_int32 r = visibleWidth % (numChannels >> 1);

        for (i = 0; i < numChannels >> 1; i++)
            channelWidthTable[i] = channelWidth;

        for (i = 0; i < r; i++)
        {
            pp_int32 j = (i * (numChannels >> 1)) / r;
            channelWidthTable[j]++;
        }
    }
    else
    {
        pp_int32 i;
        pp_int32 r = visibleWidth % numChannels;

        for (i = 0; i < numChannels; i++)
            channelWidthTable[i] = channelWidth;

        for (i = 0; i < r; i++)
        {
            pp_int32 j = (i * numChannels) / r;
            channelWidthTable[j]++;
        }
    }

    pp_int32 cn = 0;
    pp_int32 y = 0;
    pp_int32 x = 0;

    // calculate shading for scopes
    PPColor scopebColor = TrackerConfig::colorScopes, scopedColor = TrackerConfig::colorScopes;
    // adjust dark color
    scopedColor.scaleFixed(32768);
    PPColor tempCol = PPUIConfig::getInstance()->getColor(PPUIConfig::ColorListBoxBackground);
    tempCol.scaleFixed(32768);
    scopedColor += tempCol;
    scopedColor.scaleFixed(49192);

    for (pp_int32 c = 0; c < numChannels; c++)
    {
        char buffer[8];
        PPTools::convertToDec(buffer, c + 1, PPTools::getDecNumDigits(c + 1));

        mp_sint32 locy = location.y + yOffset + y * channelHeight + channelHeight / 2;
        mp_sint32 locx = location.x + xOffset + x;
        mp_sint32 cWidth = channelWidthTable[cn];

        pp_int32 count = cWidth;

        // correct last channel
        cn++;
        if (numChannels > WRAPCHANNELS() && cn >= (numChannels >> 1))
        {
            cn = 0;
            y++;
            count = visibleWidth - (xOffset + x);
        }
        else if (c == numChannels - 1)
        {
            count = visibleWidth - (xOffset + x);
        }

        g->setColor(TrackerConfig::colorScopes);

        pp_int32 sx = locx + 3;
        pp_int32 sy = locy - channelHeight / 2 + 3;

        pp_int32 sy2 = locy + channelHeight / 2 - smallFont->getCharHeight() - 1;

        if (!muteChannels[c])
        {
            ScopePainter scopePainter(g, count, channelHeight, scopebColor, scopedColor, locx, locy, appearance);

            if (enabled)
                playerController->grabSampleData(c, count, 160, scopePainter);
            else
            {
                for (pp_int32 i = 0; i < count; i++)
                    scopePainter.fetchSampleData(0);
            }

            #ifdef PANNINGINDICATOR
            pp_int32 panx = locx;
            pp_int32 they = sy;

            g->drawHLine(panx + 3, panx + count2, they + channelHeight - 8);
            g->drawHLine(panx + 3, panx + count2, they + channelHeight - 5);
            g->drawVLine(they + channelHeight - 8, they + channelHeight - 5 + 1, panx + 2);
            g->drawVLine(they + channelHeight - 8, they + channelHeight - 5 + 1, panx + count2);

            PPColor col = g->getColor();
            g->setColor(TrackerConfig::colorHighLight);
            pp_int32 thex = panx + 3 + (channel.pan * (count2 - 5)) / 255;
            g->drawVLine(they + channelHeight - 7, they + channelHeight - 6 + 1, thex);
            g->drawVLine(they + channelHeight - 7, they + channelHeight - 6 + 1, thex + 1);
            g->setColor(col);
            #endif
            locx = scopePainter.getLocx();
        }
        else
        {
            PPFont* font = this->font;
            PPColor col = g->getColor();
            const char* muteStr = "MUTE";
            if ((signed)font->getStrWidth(muteStr) >= cWidth - 2)
                font = smallFont;
            if ((signed)font->getStrWidth(muteStr) >= cWidth - 2)
                muteStr = "<M>";
            pp_int32 x = locx + (cWidth / 2) - font->getStrWidth(muteStr) / 2;
            pp_int32 y = locy - font->getCharHeight() / 2;
            g->setColor(TrackerConfig::colorHighLight_1);
            g->setFont(font);
            g->drawString(muteStr, x, y);
            g->setColor(col);
        }

        if (recChannels[c])
        {
            g->setFont(smallFont);
            g->setColor(0, 0, 0);
            g->drawString("\xf0", sx, sy2 + 1);
            g->setColor(TrackerConfig::colorRecordModeButtonText);
            g->drawString("\xf0", sx - 1, sy2);
        }

        g->setFont((channelWidth <40 || numChannels>WRAPCHANNELS()) ? smallFont : font);
        g->setColor(0, 0, 0);
        g->drawString(buffer, sx + 1, sy + 1);
        g->setColor(foregroundColor);
        g->drawString(buffer, sx, sy);

        if (cn == 0)
            x = -cWidth;

        x += cWidth;

    }

    g->setRect(location.x + 1, location.y + 1, location.x + size.width - 1, location.y + size.height - 1);
    x = location.x + xOffset + channelWidthTable[0];
    for (cn = 0; cn < (visibleWidth / channelWidth) - 1; cn++)
    {
        g->setColor(bColor);
        g->drawVLine(location.y + yOffset - 1, location.y + yOffset + visibleHeight, x - 1);
        g->setColor(*borderColor);
        g->drawVLine(location.y + yOffset - 1, location.y + yOffset + visibleHeight, x);
        g->setColor(dColor);
        g->drawVLine(location.y + yOffset, location.y + yOffset + visibleHeight, x + 1);
        x += channelWidthTable[cn + 1];
    }

    if (numChannels > WRAPCHANNELS())
    {
        x = location.x + xOffset;

        for (cn = 0; cn < (visibleWidth / channelWidth); cn++)
        {
            g->setColor(bColor);
            g->drawHLine(x + 1, x + channelWidthTable[cn] - 1, location.y + yOffset + (visibleHeight >> 1) - 1);

            g->setColor(*borderColor);
            g->drawHLine(x, x + channelWidthTable[cn] - 1, location.y + yOffset + (visibleHeight >> 1));

            g->setColor(dColor);
            g->drawHLine(x + 1, x + channelWidthTable[cn] - 1, location.y + yOffset + (visibleHeight >> 1) + 1);

            x += channelWidthTable[cn];
        }
    }

}

void ScopesControl::attachSource(PlayerController* playerController)
{
    this->playerController = playerController;
    // should force redraw
    lastNumChannels = 0;
}

bool ScopesControl::needsUpdate()
{
    if (playerController == NULL ||
        (numChannels == 0 && lastNumChannels == 0))
        return false;

    if (numChannels != lastNumChannels)
    {
        lastNumChannels = numChannels;
        return true;
    }

    bool res = false;
    for (pp_int32 c = 0; c < numChannels; c++)
    {
        if (muteChannels[c] != lastMuteChannels[c])
        {
            lastMuteChannels[c] = muteChannels[c];
            res = true;
        }

        if (recChannels[c] != lastRecChannels[c])
        {
            lastRecChannels[c] = recChannels[c];
            res = true;
        }

        bool bPlay = enabled ? playerController->hasSampleData(c) : false;

        if (bPlay != onOffState[c] || bPlay)
            res = true;

        onOffState[c] = bPlay;
    }

    return res;
}

void ScopesControl::handleMute(pp_int32 channel)
{
    muteChannels[channel] = !muteChannels[channel];
    PPEvent e(eValueChanged, &muteChannels, sizeof(muteChannels), ChangeValueMuting);
    eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
}

void ScopesControl::handleSolo(pp_int32 channel)
{
    // unmute?
    if (isSoloChannel(channel))
    {
        for (pp_uint32 i = 0; i < sizeof(muteChannels) / sizeof(pp_uint8); i++)
            muteChannels[i] = false;
    }
    else
    {
        for (pp_uint32 i = 0; i < sizeof(muteChannels) / sizeof(pp_uint8); i++)
            muteChannels[i] = true;
        muteChannels[channel] = false;
    }
    PPEvent e(eValueChanged, &muteChannels, sizeof(muteChannels), ChangeValueMuting);
    eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
}

void ScopesControl::handleRec(pp_int32 channel)
{
    recChannels[channel] = !recChannels[channel];
    PPEvent e(eValueChanged, &recChannels, sizeof(recChannels), ChangeValueRecording);
    eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
}

void ScopesControl::handleSingleRec(pp_int32 channel)
{
    // unmute?
    if (isSingleRecChannel(channel))
    {
        for (pp_uint32 i = 0; i < sizeof(recChannels) / sizeof(pp_uint8); i++)
            recChannels[i] = true;
    }
    else
    {
        for (pp_uint32 i = 0; i < sizeof(recChannels) / sizeof(pp_uint8); i++)
            recChannels[i] = false;
        recChannels[channel] = true;
    }
    PPEvent e(eValueChanged, &recChannels, sizeof(recChannels), ChangeValueRecording);
    eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
}

void ScopesControl::handleUnmuteAll()
{
    for (pp_uint32 i = 0; i < sizeof(muteChannels) / sizeof(pp_uint8); i++)
        muteChannels[i] = false;
    PPEvent e(eValueChanged, &muteChannels, sizeof(muteChannels), ChangeValueMuting);
    eventListener->handleEvent(reinterpret_cast<PPObject*>(this), &e);
}

pp_int32 ScopesControl::dispatchEvent(PPEvent* event)
{
    if (eventListener == NULL)
        return -1;

    switch (event->getID())
    {
        case eLMouseDown:
            didSoloChannel = false;
            lMouseDownInChannel = pointToChannel(*(PPPoint*)event->getDataPtr());
            break;

        case eRMouseDown:
            didSoloChannel = false;
            rMouseDownInChannel = pointToChannel(*(PPPoint*)event->getDataPtr());
            break;

        case eLMouseUp:
        {
            pp_int32 channel = pointToChannel(*(PPPoint*)event->getDataPtr());

            // left-right combined mouse click
            if (channel != -1 && lMouseDownInChannel == channel && rMouseDownInChannel == channel)
            {
                handleSolo(channel);
                didSoloChannel = true;
            }
            // normal click
            else if (channel != -1 && lMouseDownInChannel == channel && rMouseDownInChannel == -1)
            {
                switch (currentClickType)
                {
                    case ClickTypeMute:
                        handleMute(channel);
                        break;
                    case ClickTypeSolo:
                        handleSolo(channel);
                        break;
                    case ClickTypeRec:
                        handleRec(channel);
                        break;
                    case ClickTypeSingleRec:
                        handleSingleRec(channel);
                        break;
                    default:
                        ASSERT(false);
                }
            }

            lMouseDownInChannel = -1;
            break;
        }

        case eRMouseUp:
        {
            pp_int32 channel = pointToChannel(*(PPPoint*)event->getDataPtr());

            if (channel != -1 && channel == rMouseDownInChannel && lMouseDownInChannel == -1 && !didSoloChannel)
            {
                if ((::getKeyModifier() & KeyModifierSHIFT))
                {
                    handleSingleRec(channel);
                }
                else handleRec(channel);
            }

            rMouseDownInChannel = -1;
            break;
        }

    }

    return 0;
}

bool ScopesControl::isSoloChannel(pp_int32 c) const
{
    pp_int32 i = 0;
    for (pp_int32 j = 0; j < numChannels; j++)
        if (muteChannels[j])
            i++;

    if (!muteChannels[c] && i == numChannels - 1)
        return true;

    return false;
}

bool ScopesControl::isSingleRecChannel(pp_int32 c) const
{
    pp_int32 i = 0;
    for (pp_int32 j = 0; j < numChannels; j++)
        if (!recChannels[j])
            i++;

    if (recChannels[c] && i == numChannels - 1)
        return true;

    return false;
}

pp_int32 ScopesControl::pointToChannel(const PPPoint& pt)
{
    if (numChannels < 2)
        return -1;

    pp_int32 xOffset = 2;
    pp_int32 yOffset = 2;

    pp_int32 channelWidth = visibleWidth / numChannels;
    pp_int32 channelHeight = visibleHeight;

    if (numChannels > WRAPCHANNELS())
    {
        channelWidth = visibleWidth / (numChannels >> 1);
        channelHeight >>= 1;

        pp_int32 i;
        pp_int32 r = visibleWidth % (numChannels >> 1);

        for (i = 0; i < numChannels >> 1; i++)
            channelWidthTable[i] = channelWidth;

        for (i = 0; i < r; i++)
        {
            pp_int32 j = (i * (numChannels >> 1)) / r;
            channelWidthTable[j]++;
        }
    }
    else
    {
        pp_int32 i;
        pp_int32 r = visibleWidth % numChannels;

        for (i = 0; i < numChannels; i++)
            channelWidthTable[i] = channelWidth;

        for (i = 0; i < r; i++)
        {
            pp_int32 j = (i * numChannels) / r;
            channelWidthTable[j]++;
        }
    }


    // detect hit
    pp_int32 cn = 0;
    pp_int32 y = 0;
    pp_int32 x = 0;

    for (pp_int32 c = 0; c < numChannels; c++)
    {
        mp_sint32 locy = location.y + yOffset + y * channelHeight;
        mp_sint32 locx = location.x + xOffset + x;
        mp_sint32 cWidth = channelWidthTable[cn];

        pp_int32 count = cWidth;

        // correct last channel
        cn++;
        if (numChannels > WRAPCHANNELS() && cn >= (numChannels >> 1))
        {
            cn = 0;
            y++;
            count += visibleWidth - (xOffset + x);
        }
        else if (c == numChannels - 1)
        {
            count += visibleWidth - (xOffset + x);
        }

        if (pt.x > locx + 1 && pt.y + 1 > locy && pt.x < locx + count - 1 && pt.y < locy + channelHeight - 1)
            return c;

        if (cn == 0)
            x = -cWidth;

        x += cWidth;
    }

    return -1;
}